﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ScopePropertyBehavior.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Windows
{
    using System;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Windows;
    using Collections;

    internal class ScopePropertyBehavior
    {
        #region Fields

        private static readonly DependencyProperty BehaviorCollectionProperty = DependencyProperty.RegisterAttached(
            "BehaviorCollection", typeof(Collection<ScopePropertyBehavior>), typeof(ScopePropertyBehavior), null);

        private WeakCollection<ScopePropertyObserver> _observers;

        #endregion

        private ScopePropertyBehavior(DependencyProperty property)
        {
            Property = property;
        }

        /// <summary>
        /// Gets or sets the <see cref="DependencyProperty"/> the scope is monitoring
        /// </summary>
        /// <value>The property.</value>
        private DependencyProperty Property { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance has a value.
        /// </summary>
        /// <value><c>true</c> if this instance has value; otherwise, <c>false</c>.</value>
        private bool HasValue { get; set; }

        /// <summary>
        /// Gets a value indicating whether this instance has clients.
        /// </summary>
        /// <value><c>true</c> if this instance has clients; otherwise, <c>false</c>.</value>
        private bool HasClients
        {
            get { return _observers != null && _observers.Count > 0; }
        }

        /// <summary>
        /// Properties the changed.
        /// </summary>
        /// <param name="dependencyObject">The dependency object.</param>
        /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        /// <param name="propertyMetadata">The metadata.</param>
        internal static void ValueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e, 
            ScopePropertyMetadata propertyMetadata)
        {
            if (IsValueCleared(dependencyObject, e.Property, propertyMetadata, e.NewValue))
            {
                ValueCleared(dependencyObject, e.Property);
            }
            else
            {
                ValueSet(dependencyObject, e.Property);
            }
        }

        private static bool IsValueCleared(DependencyObject dependencyObject, DependencyProperty property, 
            ScopePropertyMetadata propertyMetadata, object value)
        {
            var unsetValue = propertyMetadata.UnsetValue;
            if (ReferenceEquals(unsetValue, DependencyProperty.UnsetValue) && 
                ReferenceEquals(dependencyObject.ReadLocalValue(property), DependencyProperty.UnsetValue))
            {
                return true;
            }

            return Equals(value, unsetValue);
        }

        private static void ValueSet(DependencyObject obj, DependencyProperty property)
        {
            bool wasCreated;

            var scope = GetOrCreateBehavior(obj, property, out wasCreated);
            if (wasCreated)
            {
                scope.HasValue = true;
                scope.OnInvalidateParentSource(obj);
            }
            else
            {
                scope.OnValueChanged(obj);
            }
        }

        private static void ValueCleared(DependencyObject obj, DependencyProperty property)
        {
            var scope = GetBehavior(obj, property);
            if (scope != null)
            {
                if (obj != ScopePropertyFallback.Instance)
                {
                    if (scope.HasValue)
                    {
                        scope.OnInvalidateSource(obj);
                    }

                    RemoveScope(obj, scope);
                }
                else
                {
                    scope.OnValueChanged(obj);
                }
            }
        }

        /// <summary>
        /// Adds a <see cref="ScopePropertyObserver"/>.
        /// </summary>
        /// <param name="source">The value source.</param>
        /// <param name="observer">The observer.</param>
        internal static void AddObserver(DependencyObject source, ScopePropertyObserver observer)
        {
            var scope = GetOrCreateBehavior(source, observer.Property.Property);
            if (!scope.HasValue && source != ScopePropertyFallback.Instance)
            {
                throw new InvalidOperationException();
            }

            scope.AddObserver(observer);
        }

        /// <summary>
        /// Adds a <see cref="ScopePropertyObserver"/>.
        /// </summary>
        /// <param name="observer">The observer.</param>
        internal void AddObserver(ScopePropertyObserver observer)
        {
            if (_observers == null)
            {
                _observers = new WeakCollection<ScopePropertyObserver>();
            }

            _observers.Add(observer);
        }

        /// <summary>
        /// Removes a <see cref="ScopePropertyObserver"/>.
        /// </summary>
        /// <param name="client">The observer to remove.</param>
        /// <returns><c>true</c> if the observer was removed; otherwise, <c>false</c>.</returns>
        internal bool RemoveObserver(ScopePropertyObserver client)
        {
            if (!HasClients)
            {
                return false;
            }

            var result = _observers.Remove(client);
            if (result)
            {
                _observers.Compact();
                if (!HasClients)
                {
                    _observers = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Removes a <see cref="ScopePropertyObserver"/>.
        /// </summary>
        /// <param name="source">The value source.</param>
        /// <param name="observer">The observer.</param>
        /// <returns><c>true</c> if the observer was removed; otherwise, <c>false</c>.</returns>
        internal static bool RemoveObserver(DependencyObject source, ScopePropertyObserver observer)
        {
            var scope = GetBehavior(source, observer.Property.Property);
            return scope != null && scope.RemoveObserver(observer);
        }

        /// <summary>
        /// Removes the scope specified scope from the <paramref name="source"/>.
        /// </summary>
        /// <param name="source">The value source.</param>
        /// <param name="scope">The scope.</param>
        private static void RemoveScope(DependencyObject source, ScopePropertyBehavior scope)
        {
            var collection = GetBehaviorCollection(source);
            collection.Remove(scope);
            if (collection.Count == 0)
            {
                SetBehaviorCollection(source, null);
            }
        }

        /// <summary>
        /// Invalidates the value source.
        /// </summary>
        /// <param name="obj">The <see cref="DependencyObject"/> whose value source should be invalidated.</param>
        private void OnInvalidateSource(DependencyObject obj)
        {
            ForEachObserver(x => x.InvalidateSource());
        }

        /// <summary>
        /// Invalidates the parent value source.
        /// </summary>
        /// <param name="obj">The <see cref="DependencyObject"/> whose parent value source should be invalidated.</param>
        private void OnInvalidateParentSource(DependencyObject obj)
        {
            ScopePropertyBehavior scope = null;

            if (obj != ScopePropertyFallback.Instance)
            {
                var parent = ScopePropertyObserver.GetParent(obj);
                if (parent != null)
                {
                    var targetSource = ScopePropertyObserver.FindSource(parent, Property);
                    if (targetSource != null)
                    {
                        scope = GetBehavior(targetSource, Property);
                    }
                }
            }

            if (scope == null)
            {
                scope = GetBehavior(ScopePropertyFallback.Instance, Property);
            }

            if (scope != null)
            {
                scope.ForEachObserver(x => x.InvalidateSource());
            }
        }

        /// <summary>
        /// Invoked when the value is changed.
        /// </summary>
        /// <param name="obj">The obj.</param>
        private void OnValueChanged(DependencyObject obj)
        {
            ForEachObserver(x => x.OnValueChanged());
        }

        /// <summary>
        /// Performs the specified <paramref name="action"/> on each observer
        /// </summary>
        /// <param name="action">The action to perform.</param>
        private void ForEachObserver(Action<ScopePropertyObserver> action)
        {
            if (_observers != null)
            {
                // copy listeners as invoking the action may cause them to be removed.
                foreach (var observer in _observers.ToList())
                {
                    action(observer);
                }
            }
        }

        /// <summary>
        /// Gets the scope collection for the specified object.
        /// </summary>
        /// <param name="obj">The value source.</param>
        /// <returns>The scope collection.</returns>
        private static Collection<ScopePropertyBehavior> GetBehaviorCollection(DependencyObject obj)
        {
            return (Collection<ScopePropertyBehavior>)obj.GetValue(BehaviorCollectionProperty);
        }

        /// <summary>
        /// Sets the scope collection for the specified object.
        /// </summary>
        /// <param name="obj">The value source.</param>
        /// <param name="value">The value.</param>
        private static void SetBehaviorCollection(DependencyObject obj, Collection<ScopePropertyBehavior> value)
        {
            obj.SetValue(BehaviorCollectionProperty, value);
        }

        /// <summary>
        /// Gets or create the scope collection.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns>The scope collection.</returns>
        private static Collection<ScopePropertyBehavior> GetOrCreateBehaviorCollection(DependencyObject obj)
        {
            var scopeCollection = GetBehaviorCollection(obj);
            if (scopeCollection == null)
            {
                scopeCollection = new Collection<ScopePropertyBehavior>();
                obj.SetValue(BehaviorCollectionProperty, scopeCollection);
            }

            return scopeCollection;
        }

        /// <summary>
        /// Gets the <see cref="ScopePropertyBehavior"/> for the specified <paramref name="obj"/> and <paramref name="property"/>.
        /// </summary>
        /// <param name="obj">The object the scope targets.</param>
        /// <param name="property">The property the scope targets.</param>
        /// <returns>The scope.</returns>
        internal static ScopePropertyBehavior GetBehavior(DependencyObject obj, DependencyProperty property)
        {
            var scopeCollection = GetBehaviorCollection(obj);
            if (scopeCollection != null)
            {
                return scopeCollection.FirstOrDefault(x => x.Property == property);
            }

            return null;
        }

        /// <summary>
        /// Gets the or create scope.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <param name="property">The property.</param>
        /// <returns>The scope.</returns>
        private static ScopePropertyBehavior GetOrCreateBehavior(DependencyObject obj, DependencyProperty property)
        {
            bool wasCreated;
            return GetOrCreateBehavior(obj, property, out wasCreated);
        }

        /// <summary>
        /// Gets the or create scope.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <param name="property">The property.</param>
        /// <param name="wasCreated">if set to <c>true</c> [was created].</param>
        /// <returns>The scope.</returns>
        private static ScopePropertyBehavior GetOrCreateBehavior(DependencyObject obj, DependencyProperty property, out bool wasCreated)
        {
            wasCreated = false;

            // need scopes collection 
            var scopeCollection = GetOrCreateBehaviorCollection(obj);

            // locate or create scope
            var scope = scopeCollection.FirstOrDefault(x => x.Property == property);
            if (scope == null)
            {
                scope = new ScopePropertyBehavior(property)
                    {
                        HasValue = obj.ReadLocalValue(property) != DependencyProperty.UnsetValue
                    };
                scopeCollection.Add(scope);
                wasCreated = true;
            }

            return scope;
        }

        /// <summary>
        /// Determines whether scope has the specified observer
        /// </summary>
        /// <param name="observer">The observer.</param>
        /// <returns><c>true</c> if the scope has the specified observer; otherwise, <c>false</c>.</returns>
        internal bool HasObserver(ScopePropertyObserver observer)
        {
            return _observers != null && _observers.Contains(observer);
        }
    }
}